package com.net.io.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * 1. NIO(Non Blocking IO)
 * 同步非阻塞，服务器实现模式为一个线程可以处理多个请求(连接)，
 * 客户端发送的连接请求都会注册到 多路复用器selector 上，多路复用器轮询到连接有IO请求就进行处理。
 * <p>
 * 2. I/O多路复用底层主要用的Linux 内核·函数（select，poll，epoll）来实现，windows不支持epoll实现，windows底层是基于winsock2的select函数实现的(不开源)
 * |       |select |poll  |epoll(jdk 1.5及以上)|
 * 操作方式 |遍历    |遍历   |回调
 * 底层实现 |数组    |链表   |哈希表
 * IO效率  |每次调用都进行线性遍历，时间复杂度为O(n) |每次调用都进行线性遍历，时间复杂度为O(n) |事件通知方式，每当有IO事件就绪，系统注册的回调函数就会被调用，时间复杂度O(1)
 * 最大连接 |有上限  |无上限 |无上限
 * <p>
 * 3. 应用场景：
 * NIO方式适用于连接数目多且连接比较短（轻操作） 的架构， 比如聊天服务器， 弹幕系统。
 * 服务器间通讯，编程比较复杂， JDK1.4 开始支持
 * <p>
 * 4. NIO 有三大核心组件： Channel(通道)， Buffer(缓冲区)，Selector(选择器)
 * (1) channel 类似于流，每个 channel 对应一个 buffer缓冲区，buffer 底层就是个数组
 * (2) channel 会注册到 selector 上，由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理
 * (3) selector 可以对应一个或多个线程
 * (4) NIO 的 Buffer 和 channel 都是既可以读也可以写
 * client - buffer - SocketChannel - 注册 - |
 * client - buffer - SocketChannel - 注册 - selector(多路复用器、就绪事件列表) -> Thread <- server
 * client - buffer - SocketChannel - 注册 - |           | --  注册  -- ServerSocketChannel
 * <p>
 * 5. Redis就是典型的NIO线程模型，selector收集所有连接的事件并且转交给后端线程，线程连续执行所有事件命令并将结果写回客户端
 * Redis就是典型的基于epoll的NIO线程模型(nginx也是)，epoll实例收集所有事件(连接与读写事件)，由一个服务端线程连续处理所有事件命令。
 *
 * @author zhangtengfei
 * @date 2021/7/29 15:09
 */

public class NioSelectorServer {
    /**
     * 总结：NIO模型的selector 就像一个大总管，负责监听各种IO事件，然后转交给后端线程去处理
     * 1. NIO相对于BIO非阻塞的体现就在，BIO的后端线程需要阻塞等待客户端写数据(比如read方法)，如果客户端不写数据线程就要阻塞，
     * 2. NIO把等待客户端操作的事情交给了大总管 selector，selector 负责轮询所有已注册的客户端，发现有事件发生了才转交给后端线程处理，
     * 后端线程不需要做任何阻塞等待，直接处理客户端事件的数据即可，处理完马上结束，或返回线程池供其他客户端事件继续使用。
     * 还有就是 channel 的读写是非阻塞的。
     * <p>
     * <p>
     * NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现，跟上面的NioServer代码类似，
     * selector每次都会轮询所有的sockchannel看下哪个channel有读写事件，有的话就处理，没有就继续遍历。
     * JDK1.5开始引入了epoll基于事件响应机制来优化NIO。
     * <p>
     * <p>
     * 总结：NIO整个调用流程就是Java调用了操作系统的内核函数来创建Socket，获取到Socket的文件描述符；
     * 再创建一个Selector对象，对应操作系统的Epoll描述符；
     * 将获取到的Socket连接的文件描述符的事件绑定到Selector对应的Epoll文件描述符上，进行事件的异步通知，
     * 这样就实现了使用一条线程，并且不需要太多的无效的遍历，将事件处理交给了操作系统内核(操作系统中断程序实现)，大大提高了效率。
     */
    public static void main(String[] args) throws IOException, InterruptedException {

        // 创建NIO ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        serverSocketChannel.configureBlocking(false);
        /**
         * 打开Selector处理Channel，即创建 epoll
         * //创建多路复用器
         */
        Selector selector = Selector.open();
        /**
         * 把ServerSocketChannel注册到selector上，并且selector对客户端accept连接操作感兴趣
         * //将channel注册到多路复用器上
         */
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动成功");

        while (true) {
            System.out.println("等待事件发生。。");
            /**
             * 阻塞
             * //阻塞等待需要处理的事件发生
             * 轮询监听channel里的key，select是阻塞的，accept()也是阻塞的
             */
            selector.select();

            // 获取selector中注册的全部事件的 SelectionKey 实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            // 有客户端请求，被轮询监听到
            System.out.println("有事件发生了。。");

            // 遍历SelectionKey对事件进行处理
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    System.out.println("有客户端连接事件发生了。。");
                    /**
                     * 如果是OP_ACCEPT事件，则进行连接获取和事件注册
                     */
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    /**
                     * NIO非阻塞体现：
                     * 此处accept方法是阻塞的，但是这里因为是发生了连接事件，会马上执行完
                     * 处理完连接请求不会继续等待客户端的数据发送
                     */
                    SocketChannel socketChannel = server.accept();
                    socketChannel.configureBlocking(false);
                    // 这里只注册了读事件，如果需要给客户端发送数据可以注册写事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                } else if (key.isReadable()) {
                    System.out.println("有客户端数据可读事件发生了。。");
                    /**
                     * 如果是OP_READ事件，则进行读取和打印
                     */
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int len = socketChannel.read(byteBuffer);
                    // 如果有数据，把数据打印出来
                    if (len > 0) {
                        System.out.println("接收到消息：" + new String(byteBuffer.array()));
                    } else if (len == -1) {
                        /**
                         * 如果客户端断开连接，关闭Socket
                         */
                        System.out.println("客户端断开连接");
                        socketChannel.close();
                    }
                } else if (key.isWritable()) {
                    SocketChannel sc = (SocketChannel) key.channel();
                    System.out.println("write事件");
                    // NIO事件触发是水平触发
                    // 使用Java的NIO编程的时候，在没有数据可以往外写的时候要取消写事件，
                    // 在有数据往外写的时候再注册写事件
                    key.interestOps(SelectionKey.OP_READ);
                    //sc.close();
                }
                //从事件集合里删除本次处理的key，防止下次select重复处理
                iterator.remove();
            }
        }
    }
}
